package flare.physics
{
  import flash.geom.Rectangle;

  /**
   * A physical simulation involving particles, springs, and forces.
   * Useful for simulating a range of physical effects or layouts.
   */
  public class Simulation
  {
    private var _particles:Vector.<Particle> = new Vector.<Particle>();
    private var _springs:Vector.<Spring> = new Vector.<Spring>();
    private var _forces:Vector.<IForce> = new Vector.<IForce>();
    private var _bounds:Rectangle = null;

    /** The default gravity force for this simulation. */
    public function get gravityForce():GravityForce
    {
      return _forces[0] as GravityForce;
    }

    /** The default n-body force for this simulation. */
    public function get nbodyForce():NBodyForce
    {
      return _forces[1] as NBodyForce;
    }

    /** The default drag force for this simulation. */
    public function get dragForce():DragForce
    {
      return _forces[2] as DragForce;
    }

    /** The default spring force for this simulation. */
    public function get springForce():SpringForce
    {
      return _forces[3] as SpringForce;
    }

    /** Sets a bounding box for particles in this simulation.
     *  Null (the default) indicates no boundaries. */
    public function get bounds():Rectangle
    {
      return _bounds;
    }

    public function set bounds(b:Rectangle):void
    {
      if(_bounds == b)return;

      if(b == null)
      {
        _bounds = null;
        return;
      }

      if(_bounds == null)
      {
        _bounds = new Rectangle();
      }
      // ensure x is left-most and y is top-most
      _bounds.x = b.x + (b.width < 0 ? b.width : 0);
      _bounds.width = (b.width < 0 ? -1 : 1) * b.width;
      _bounds.y = b.y + (b.width < 0 ? b.height : 0);
      _bounds.height = (b.height < 0 ? -1 : 1) * b.height;
    }

    /**
     * Creates a new physics simulation.
     * @param gx the gravitational acceleration along the x dimension
     * @param gy the gravitational acceleration along the y dimension
     * @param drag the default drag (viscosity) co-efficient
     * @param attraction the gravitational attraction (or repulsion, for
     *  negative values) between particles.
     */
    public function Simulation(gx:Number = 0, gy:Number = 0, drag:Number = 0.1, attraction:Number = - 5)
    {
      _forces.push(new GravityForce(gx, gy));
      _forces.push(new NBodyForce(attraction));
      _forces.push(new DragForce(drag));
      _forces.push(new SpringForce());
    }

    // -- Init Simulation -------------------------------------------------

    /**
     * Adds a custom force to the force simulation.
     * @param force the force to add
     */
    public function addForce(force:IForce):void
    {
      _forces.push(force);
    }

    /**
     * Returns the force at the given index.
     * @param idx the index of the force to look up
     * @return the force at the specified index
     */
    public function getForceAt(idx:int):IForce
    {
      return _forces[idx];
    }

    /**
     * Adds a new particle to the simulation.
     * @param mass the mass (charge) of the particle
     * @param x the particle's starting x position
     * @param y the particle's starting y position
     * @return the added particle
     */
    public function addParticle(mass:Number, x:Number, y:Number):Particle
    {
      var p:Particle = getParticle(mass, x, y);
      _particles.push(p);
      return p;
    }

    /**
     * Removes a particle from the simulation. Any springs attached to
     * the particle will also be removed.
     * @param idx the index of the particle in the particle list
     * @return true if removed, false otherwise.
     */
    public function removeParticle(idx:uint):Boolean
    {
      var p:Particle = _particles[idx];

      if(p == null)return false;

      // remove springs
      for(var i:uint = _springs.length; --i >= 0; )
      {
        var s:Spring = _springs[i];

        if(s.p1 == p || s.p2 == p)
        removeSpring(i);
      }
      // remove from particles
      reclaimParticle(p);
      _particles.splice(idx, 1);
      return true;
    }

    /**
     * Adds a spring to the simulation
     * @param p1 the first particle attached to the spring
     * @param p2 the second particle attached to the spring
     * @param restLength the rest length of the spring
     * @param tension the tension of the spring
     * @param damping the damping (friction) co-efficient of the spring
     * @return the added spring
     */
    public function addSpring(p1:Particle, p2:Particle, restLength:Number, tension:Number, damping:Number):Spring
    {
      var s:Spring = getSpring(p1, p2, restLength, tension, damping);
      p1.degree++;
      p2.degree++;
      _springs.push(s);
      return s;
    }


    /**
     * Removes a spring from the simulation.
     * @param idx the index of the spring in the spring list
     * @return true if removed, false otherwise
     */
    public function removeSpring(idx:uint):Boolean
    {
      if(idx >= _springs.length)return false;
      var s:Spring = _springs[idx];
      s.p1.degree--;
      s.p2.degree--;
      reclaimSpring(s);
      _springs.splice(idx, 1);
      return true;
    }

    /**
     * Returns the particle list. This is the same vector instance backing
     * the simulation, so edit the vector with caution.
     * @return the particle list as a Vector.<Particle> object
     */
    public function get particles():Vector.<Particle>
    {
      return _particles;
    }

    /**
     * Returns the spring list. This is the same vector instance backing
     * the simulation, so edit the vector with caution.
     * @return the spring list as a Vector.<Spring> object
     */
    public function get springs():Vector.<Spring>
    {
      return _springs;
    }

    // -- Run Simulation --------------------------------------------------

    /**
     * Advance the simulation for the specified time interval.
     * @param dt the time interval to step the simulation (default 1)
     */
    public function tick(dt:Number = 1):void
    {
      var p:Particle, s:Spring, i:uint, ax:Number, ay:Number;
      var dt1:Number = dt / 2, dt2:Number = dt * dt / 2;

      // remove springs connected to dead particles
      for(i = _springs.length; --i >= 0; )
      {
        s = _springs[i];

        if(s.die || s.p1.die || s.p2.die)
        {
          s.p1.degree--;
          s.p2.degree--;
          reclaimSpring(s);
          _springs.splice(i, 1);
        }
      }

      // update particles using Verlet integration
      for(i = _particles.length; --i >= 0; )
      {
        p = _particles[i];
        p.age += dt;

        if(p.die)
        { // remove dead particles
          reclaimParticle(p);
          _particles.splice(i, 1);
        }

        else if(p.fixed)
        {
          p.vx = p.vy = 0;
        }

        else
        {
          ax = p.fx / p.mass;
          ay = p.fy / p.mass;
          p.x += p.vx * dt + ax * dt2;
          p.y += p.vy * dt + ay * dt2;
          p._vx = p.vx + ax * dt1;
          p._vy = p.vy + ay * dt1;
        }
      }
      // evaluate the forces
      eval();

      // update particle velocities
      for(i = _particles.length; --i >= 0; )
      {
        p = _particles[i];

        if(!p.fixed)
        {
          ax = dt1 / p.mass;
          p.vx = p._vx + p.fx * ax;
          p.vy = p._vy + p.fy * ax;
        }
      }

      // enfore bounds
      if(_bounds)enforceBounds();
    }

    private function enforceBounds():void
    {
      var minX:Number = _bounds.x;
      var maxX:Number = _bounds.x + _bounds.width;
      var minY:Number = _bounds.y;
      var maxY:Number = _bounds.y + _bounds.height;

      for each(var p:Particle in _particles)
      {
        if(p.x < minX)
        {
          p.x = minX;
          p.vx = 0;
        }
        else if(p.x > maxX)
        {
          p.x = maxX;
          p.vx = 0;
        }

        if(p.y < minY)
        {
          p.y = minY;
          p.vy = 0;
        }
        else if(p.y > maxY)
        {
          p.y = maxY;
          p.vy = 0;
        }
      }
    }

    /**
     * Evaluates the set of forces in the simulation.
     */
    public function eval():void
    {
      var i:uint, p:Particle;

      // reset forces
      for(i = _particles.length; --i >= 0; )
      {
        p = _particles[i];
        p.fx = p.fy = 0;
      }

      // collect forces
      for(i = 0; i < _forces.length; ++i)
      {
        IForce(_forces[i]).apply(this);
      }
    }

    // -- Particle Pool ---------------------------------------------------

    /** The maximum number of items stored in a simulation object pool. */
    public static var objectPoolLimit:int = 5000;
    protected static var _ppool:Vector.<Particle> = new Vector.<Particle>();
    protected static var _spool:Vector.<Spring> = new Vector.<Spring>();

    /**
     * Returns a particle instance, pulling a recycled particle from the
     * object pool if available.
     * @param mass the mass (charge) of the particle
     * @param x the particle's starting x position
     * @param y the particle's starting y position
     * @return a particle instance
     */
    protected static function getParticle(mass:Number, x:Number, y:Number):Particle
    {
      if(_ppool.length > 0)
      {
        var p:Particle = _ppool.pop();
        p.init(mass, x, y);
        return p;
      }

      else
      {
        return new Particle(mass, x, y);
      }
    }

    /**
     * Returns a spring instance, pulling a recycled spring from the
     * object pool if available.
     * @param p1 the first particle attached to the spring
     * @param p2 the second particle attached to the spring
     * @param restLength the rest length of the spring
     * @param tension the tension of the spring
     * @param damping the damping (friction) co-efficient of the spring
     * @return a spring instance
     */
    protected static function getSpring(p1:Particle, p2:Particle, restLength:Number, tension:Number,
      damping:Number):Spring
    {
      if(_spool.length > 0)
      {
        var s:Spring = _spool.pop();
        s.init(p1, p2, restLength, tension, damping);
        return s;
      }

      else
      {
        return new Spring(p1, p2, restLength, tension, damping);
      }
    }

    /**
     * Reclaims a particle, adding it to the object pool for recycling
     * @param p the particle to reclaim
     */
    protected static function reclaimParticle(p:Particle):void
    {
      if(_ppool.length < objectPoolLimit)
      {
        _ppool.push(p);
      }
    }

    /**
     * Reclaims a spring, adding it to the object pool for recycling
     * @param s the spring to reclaim
     */
    protected static function reclaimSpring(s:Spring):void
    {
      if(_spool.length < objectPoolLimit)
      {
        _spool.push(s);
      }
    }
  } // end of class Simulation
}